package bg.btanev.eightyfour.dao.impl;

import java.lang.reflect.Constructor;
import java.util.Date;
import java.util.List;

import org.apache.commons.lang.Validate;
import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.transform.AliasToBeanConstructorResultTransformer;
import org.hibernate.type.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import bg.btanev.eightyfour.dao.AnalyticsDAO;
import bg.btanev.eightyfour.domain.Target;
import bg.btanev.eightyfour.domain.profile.ProfileEvent;
import bg.btanev.eightyfour.shared.analytics.CountingStat;
import bg.btanev.eightyfour.shared.analytics.DayActivityStat;
import bg.btanev.eightyfour.shared.analytics.DayHourActivityStat;
import bg.btanev.eightyfour.shared.analytics.HourActivityStat;
import bg.btanev.eightyfour.shared.analytics.MonthActivityStat;
import bg.btanev.eightyfour.shared.analytics.WeekDayActivityStat;
import bg.btanev.eightyfour.shared.analytics.YearActivityStat;

/**
 *
 * @author B.Tanev
 *
 */
public class AnalyticsDAOImpl extends HibernateDaoSupport implements AnalyticsDAO {
	/**
	 * Logger for this class
	 */
	private static final Logger logger = LoggerFactory.getLogger(AnalyticsDAOImpl.class);

	@Override
	public List<DayActivityStat> dayActivityForTarget(Target target, Date begin, Date end) {
		logger.debug("dayActivityForTarget(Target) - start"); //$NON-NLS-1$

		Validate.notNull(target);

		Criteria criteria = createProfileEventForTargetCriteria(target);
		criteria.setProjection(
				Projections.projectionList()
					.add(getProfileEventDayProjection())
					.add(Projections.rowCount(), "total"))
				.addOrder(Order.desc("total"));

		Constructor<DayActivityStat> constructor = null;
		try {
			constructor = DayActivityStat.class.getDeclaredConstructor(Date.class, Long.TYPE);
		} catch (SecurityException e) {
			logger.error("dayActivityForTarget(Target) -  CRITICAL! non consistent code! cannot access constructor!", e); //$NON-NLS-1$
		} catch (NoSuchMethodException e) {
			logger.error("dayActivityForTarget(Target) -  CRITICAL! non consistent code! cannot find constructor!", e); //$NON-NLS-1$
		}

		List<DayActivityStat> result = executeStatCriteria(criteria, constructor, begin, end);

		logger.debug("dayActivityForTarget(Target) - end"); //$NON-NLS-1$
		return result;
	}

	@Override
	public List<DayHourActivityStat> dayHourActivityForTarget(Target target, Date begin, Date end) {
		logger.debug("hourActivityForTarget(Target) - start"); //$NON-NLS-1$

		Validate.notNull(target);

		Criteria criteria = createProfileEventForTargetCriteria(target);
		criteria.setProjection(
				Projections.projectionList()
					.add(getProfileEventFilterProjection(ActovityFilter.HOUR))
					.add(getProfileEventDayProjection())
					.add(Projections.rowCount(), "total"))
				.addOrder(Order.desc("total"));

		Constructor<DayHourActivityStat> constructor = null;
		try {
			constructor = DayHourActivityStat.class.getDeclaredConstructor(Double.TYPE, Date.class, Long.TYPE);
		} catch (SecurityException e) {
			logger.error("hourActivityForTarget(Target) -  CRITICAL! non consistent code! cannot access constructor!", e); //$NON-NLS-1$
		} catch (NoSuchMethodException e) {
			logger.error("hourActivityForTarget(Target) -  CRITICAL! non consistent code! cannot find constructor!", e); //$NON-NLS-1$
		}

		List<DayHourActivityStat> result = executeStatCriteria(criteria, constructor, begin, end);

		logger.debug("hourActivityForTarget(Target) - end"); //$NON-NLS-1$
		return result;
	}

	@Override
	public List<HourActivityStat> hourActivityForTarget(Target target, Date begin, Date end) {
		logger.debug("hourActivityForTarget(Target) - start"); //$NON-NLS-1$

		Validate.notNull(target);

		Criteria criteria = createProfileEventForTargetCriteria(target);
		criteria.setProjection(
				Projections.projectionList()
					.add(getProfileEventFilterProjection(ActovityFilter.HOUR))
					.add(Projections.rowCount(), "total"))
				.addOrder(Order.desc("total"));

		Constructor<HourActivityStat> constructor = null;
		try {
			constructor = HourActivityStat.class.getDeclaredConstructor(Double.TYPE, Long.TYPE);
		} catch (SecurityException e) {
			logger.error("hourActivityForTarget(Target) -  CRITICAL! non consistent code! cannot access constructor!", e); //$NON-NLS-1$
		} catch (NoSuchMethodException e) {
			logger.error("hourActivityForTarget(Target) -  CRITICAL! non consistent code! cannot find constructor!", e); //$NON-NLS-1$
		}

		List<HourActivityStat> result = executeStatCriteria(criteria, constructor, begin, end);

		logger.debug("hourActivityForTarget(Target) - end"); //$NON-NLS-1$
		return result;
	}

	@Override
	public List<WeekDayActivityStat> weekDayActivityForTarget(Target target, Date begin, Date end) {
		logger.debug("weekDayActivityForTarget(Target) - start"); //$NON-NLS-1$

		Validate.notNull(target);

		Criteria criteria = createProfileEventForTargetCriteria(target);

		criteria.setProjection(
				Projections.projectionList()
					.add(getProfileEventFilterProjection(ActovityFilter.DAY_OF_WEEK))
					.add(Projections.rowCount(), "total"))
				.addOrder(Order.desc("total"));

		Constructor<WeekDayActivityStat> constructor = null;
		try {
			constructor = WeekDayActivityStat.class.getDeclaredConstructor(Double.TYPE, Long.TYPE);
		} catch (SecurityException e) {
			logger.error("weekDayActivityForTarget(Target) -  CRITICAL! non consistent code! cannot access constructor!", e); //$NON-NLS-1$
		} catch (NoSuchMethodException e) {
			logger.error("weekDayActivityForTarget(Target) -  CRITICAL! non consistent code! cannot find constructor!", e); //$NON-NLS-1$
		}

		List<WeekDayActivityStat> result = executeStatCriteria(criteria, constructor, begin, end);

		logger.debug("weekDayActivityForTarget(Target) - end"); //$NON-NLS-1$
		return result;
	}

	@Override
	public List<MonthActivityStat> monthActivityStat(Target target, Date begin, Date end) {
		logger.debug("monthActivityStat(Target) - start"); //$NON-NLS-1$

		Validate.notNull(target);

		Criteria criteria = createProfileEventForTargetCriteria(target);
		criteria.setProjection(
				Projections.projectionList()
					.add(getProfileEventFilterProjection(ActovityFilter.MONTH))
					.add(Projections.rowCount(), "total"))
				.addOrder(Order.desc("total"));

		Constructor<MonthActivityStat> constructor = null;
		try {
			constructor = MonthActivityStat.class.getDeclaredConstructor(Double.TYPE, Long.TYPE);
		} catch (SecurityException e) {
			logger.error("monthActivityStat(Target) -  CRITICAL! non consistent code! cannot access constructor!", e); //$NON-NLS-1$
		} catch (NoSuchMethodException e) {
			logger.error("monthActivityStat(Target) -  CRITICAL! non consistent code! cannot find constructor!", e); //$NON-NLS-1$
		}

		List<MonthActivityStat> result = executeStatCriteria(criteria, constructor, begin, end);

		logger.debug("monthActivityStat(Target) - end"); //$NON-NLS-1$
		return result;
	}

	@Override
	public List<YearActivityStat> yearActivityStat(Target target, Date begin, Date end) {
		logger.debug("yearActivityStat(Target) - start"); //$NON-NLS-1$

		Validate.notNull(target);

		Criteria criteria = createProfileEventForTargetCriteria(target);
		criteria.setProjection(
				Projections.projectionList()
					.add(getProfileEventFilterProjection(ActovityFilter.YEAR))
					.add(Projections.rowCount(), "total"))
				.addOrder(Order.desc("total"));

		Constructor<YearActivityStat> constructor = null;
		try {
			constructor = YearActivityStat.class.getDeclaredConstructor(Double.TYPE, Long.TYPE);
		} catch (SecurityException e) {
			logger.error("yearActivityStat(Target) -  CRITICAL! non consistent code! cannot access constructor!", e); //$NON-NLS-1$
		} catch (NoSuchMethodException e) {
			logger.error("yearActivityStat(Target) -  CRITICAL! non consistent code! cannot find constructor!", e); //$NON-NLS-1$
		}

		List<YearActivityStat> result = executeStatCriteria(criteria, constructor, begin, end);

		logger.debug("yearActivityStat(Target) - end"); //$NON-NLS-1$
		return result;
	}

	@SuppressWarnings("deprecation")
	private Projection getProfileEventFilterProjection(ActovityFilter filter) {
		assert (filter != null);

		return Projections.sqlGroupProjection(
				String.format("date_part('%s', date_trunc('%s', pe_date)) as \"%s\"", filter.getFilterPart(), filter.getFilterTrunc(), filter.getFilterTrunc()),
				filter.getFilterTrunc(),
				new String[] { filter.getFilterTrunc() },
				new Type[] { Hibernate.DOUBLE });
	}

	@SuppressWarnings("deprecation")
	private Projection getProfileEventDayProjection() {
		return Projections.sqlGroupProjection(
				"date_trunc('day', pe_date) as \"day\"",
				"day",
				new String[] { "day" },
				new Type[] { Hibernate.DATE });
	}

	@SuppressWarnings("unchecked")
	private <CS extends CountingStat> List<CS> executeStatCriteria(Criteria criteria, Constructor<CS> constructor, Date begin, Date end) {
		logger.debug("executeStatCriteria(Criteria, Constructor<CS>) - start"); //$NON-NLS-1$

		assert (criteria != null);
		assert (constructor != null);

		if (begin != null && end != null) {
			Validate.isTrue(begin.before(end));
		}

		if (begin != null) {
			criteria.add(Restrictions.ge("date", begin));
		}

		if (end != null) {
			criteria.add(Restrictions.lt("date", end));
		}

		List<CS> result = null;
		if (constructor != null) {
			constructor.setAccessible(true);
			criteria.setResultTransformer(new AliasToBeanConstructorResultTransformer(constructor));
			result = criteria.list();
		} else {
			logger.warn("executeStatCriteria(Criteria, Constructor<CS>) - cannot initialize result objects. skipping query."); //$NON-NLS-1$
		}

		logger.debug("executeStatCriteria(Criteria, Constructor<CS>) - end"); //$NON-NLS-1$
		return result;
	}

	private Criteria createProfileEventForTargetCriteria(Target target) {
		logger.debug("createProfileEventForTargetCriteria(Target) - start"); //$NON-NLS-1$

		assert (target != null);

		Criteria criteria = getSession().createCriteria(ProfileEvent.class);
		criteria.createAlias("profile", "pe", Criteria.LEFT_JOIN);
		criteria.add(Restrictions.eq("pe.target", target));

		logger.debug("createProfileEventForTargetCriteria(Target) - end"); //$NON-NLS-1$
		return criteria;
	}

	private enum ActovityFilter {
		HOUR("hour", "hour"), MONTH("month", "month"), YEAR("year", "year"), DAY_OF_WEEK("day", "dow");

		private final String filterTrunc;
		private final String filterPart;

		private ActovityFilter(String filterTrunc, String filterPart) {
			this.filterTrunc = filterTrunc;
			this.filterPart = filterPart;
		}

		public String getFilterTrunc() {
			return filterTrunc;
		}

		public String getFilterPart() {
			return filterPart;
		}
	}
}
